﻿using Common.EF.Structs;
using Common.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Z.EntityFramework.Plus;

namespace Common.EF
{
    public class CommonContext : DbContext, ICommonContext
    {
        private readonly CommonContextParam commonContextParam = null;
        private readonly IServiceProvider provider;

        public CommonContext() : this(null, new CommonContextParamForDI())
        {
        }

        public CommonContext(IServiceProvider provider, CommonContextParam contextParam)
        {
            LibCheckNullUtils.CheckNull(contextParam, "contextAccessParam");
            LibCheckNullUtils.CheckNull(contextParam.ConnString, "contextAccessParam.Code");
            LibCheckNullUtils.CheckNull(contextParam.Code, "contextAccessParam.Code");
            this.commonContextParam = contextParam;
            this.provider = provider;
        }

        public async Task<T> GetEntityAsync<T>(Expression<Func<T, bool>> predicate, string[] includes)
            where T : EntityBase, new()
        {
            var list = this.DoGetEntities<T>(predicate, includes);
            return list != null && list.Any() ? await list.FirstOrDefaultAsync() : null;
        }

        public async Task<List<T>> GetEntitiesAsync<T>(Expression<Func<T, bool>> predicate, string[] includes)
             where T : EntityBase, new()
        {
            var queryable = this.DoGetEntities<T>(predicate, includes);
            var st = await queryable.ToListAsync();
            return st;
        }

        public async Task<List<T>> GetOrderEntitiesAsync<T>(Expression<Func<T, bool>> predicate, Dictionary<string, int> orders, string[] includes)
             where T : EntityBase, new()
        {
            if (orders == null || !orders.Any())
            {
                throw new CommonException("GetOrderEntities param error");
            }

            var queryable = this.DoGetEntities<T>(predicate, includes);
            int type = 0;
            foreach (var item in orders)
            {
                queryable = queryable.SetQueryableOrder(type, item.Key, item.Value);
                type = 1;
            }

            var st = await queryable.ToListAsync();
            return st;
        }

        public async Task<ApiPagedResult<T>> GetOrderPageEntitiesAsync<T>(Expression<Func<T, bool>> predicate, Dictionary<string, int> orders, int startIndex, int pageSize, string[] includes)
             where T : EntityBase, new()
        {
            if (orders == null || !orders.Any())
            {
                throw new CommonException("GetOrderPageEntities param error");
            }

            if (startIndex < 0 || pageSize < 1)
            {
                throw new ArgumentOutOfRangeException("GetOrderPageEntities page param error");
            }

            var queryable = this.DoGetEntities<T>(predicate, includes);
            int type = 0;
            foreach (var item in orders)
            {
                queryable = queryable.SetQueryableOrder(type, item.Key, item.Value);
                type = 1;
            }

            var count = await queryable.CountAsync();
            ApiPagedResult<T> result = new ApiPagedResult<T>();
            if (count > 0)
            {
                result.TotalCount = count;
                result.Data = queryable.Skip(startIndex).Take(pageSize).ToList();
            }

            return result;
        }

        public T GetEntity<T>(Expression<Func<T, bool>> predicate, string[] includes)
            where T : EntityBase, new()
        {
            var list = this.DoGetEntities<T>(predicate, includes);
            return list != null && list.Any() ? list.FirstOrDefault() : null;
        }

        public List<T> GetEntities<T>(Expression<Func<T, bool>> predicate, string[] includes)
             where T : EntityBase, new()
        {
            var queryable = this.DoGetEntities<T>(predicate, includes);
            return queryable.ToList();
        }

        public List<T> GetOrderEntities<T>(Expression<Func<T, bool>> predicate, Dictionary<string, int> orders, string[] includes)
             where T : EntityBase, new()
        {
            if (orders == null || !orders.Any())
            {
                throw new CommonException("GetOrderEntities param error");
            }

            var queryable = this.DoGetEntities<T>(predicate, includes);
            int type = 0;
            foreach (var item in orders)
            {
                queryable = queryable.SetQueryableOrder(type, item.Key, item.Value);
                type = 1;
            }

            return queryable.ToList();
        }

        public ApiPagedResult<T> GetOrderPageEntities<T>(Expression<Func<T, bool>> predicate, Dictionary<string, int> orders, int startIndex, int pageSize, string[] includes)
             where T : EntityBase, new()
        {
            if (orders == null || !orders.Any())
            {
                throw new CommonException("GetOrderPageEntities param error");
            }

            if (startIndex < 0 || pageSize < 1)
            {
                throw new ArgumentOutOfRangeException("GetOrderPageEntities page param error");
            }

            var queryable = this.DoGetEntities<T>(predicate, includes);
            int type = 0;
            foreach (var item in orders)
            {
                queryable = queryable.SetQueryableOrder(type, item.Key, item.Value);
                type = 1;
            }

            var count = queryable.Count();
            ApiPagedResult<T> result = new ApiPagedResult<T>();
            if (count > 0)
            {
                result.TotalCount = count;
                result.Data = queryable.Skip(startIndex).Take(pageSize).ToList();
            }

            return result;
        }

        public int GetCount<T>(Expression<Func<T, bool>> predicate)
            where T : EntityBase, new()
        {
            return DoGetEntities<T>(predicate, null).Count();
        }

        public async Task<int> GetCountAsync<T>(Expression<Func<T, bool>> predicate)
            where T : EntityBase, new()
        {
            return await DoGetEntities<T>(predicate, null).CountAsync();
        }

        public decimal GetSum<T>(Expression<Func<T, bool>> predicate, Expression<Func<T, decimal>> sumExpression)
            where T : EntityBase, new()
        {
            return DoGetEntities<T>(predicate, null).Sum(sumExpression);
        }

        public async Task<decimal> GetSumAsync<T>(Expression<Func<T, bool>> predicate, Expression<Func<T, decimal>> sumExpression)
            where T : EntityBase, new()
        {
            return await DoGetEntities<T>(predicate, null).SumAsync(sumExpression);
        }

        public List<IGrouping<S, T>> GetGroupBy<T, S>(Expression<Func<T, bool>> predicate, Expression<Func<T, S>> groupBy)
            where T : EntityBase, new()
        {
            var list = DoGetEntities<T>(predicate, null).GroupBy<T, S>(groupBy);
            return list.ToList();
        }

        public async Task<List<IGrouping<S, T>>> GetGroupByAsync<T, S>(Expression<Func<T, bool>> predicate, Expression<Func<T, S>> groupBy)
            where T : EntityBase, new()
        {
            var list = DoGetEntities<T>(predicate, null).GroupBy<T, S>(groupBy);
            return await list.ToListAsync();
        }

        public void AddEntities<T>(List<T> entities)
           where T : EntityBase, new()
        {
            if (entities == null || !entities.Any())
            {
                return;
            }

            this.ChangeTracker.AutoDetectChangesEnabled = false;
            try
            {
                this.AddRange(entities);
            }
            finally
            {
                this.ChangeTracker.AutoDetectChangesEnabled = true;
            }
        }

        public void UpdateEntities<T>(List<T> entities, List<T> entitiesDb, params string[] updateProperies)
           where T : EntityBase, new()
        {
            if (entities == null || !entities.Any())
            {
                return;
            }

            foreach (var entity in entities)
            {
                var entityDb = entitiesDb?.FirstOrDefault(t => t.id == entity.id);
                if (entityDb != null && entityDb.row_version != entity.row_version)
                {
                    throw new EntityUpdateVersionException(string.Empty, "Update error,Please get and try again");
                }

                entity.row_version++;
            }

            this.ChangeTracker.AutoDetectChangesEnabled = false;
            try
            {
                UpdateRange(entities);
            }
            finally
            {
                this.ChangeTracker.AutoDetectChangesEnabled = true;
            }
        }

        public void UpdateEntitiesByByCommit<T>(Expression<Func<T, bool>> predicate, Expression<Func<T, T>> updateExpression)
            where T : EntityBase, new()
        {
            if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            DoGetEntities<T>(predicate, null).Update(updateExpression);
        }

        public async Task UpdateEntitiesByByCommitAsync<T>(Expression<Func<T, bool>> predicate, Expression<Func<T, T>> updateExpression)
            where T : EntityBase, new()
        {
            if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            await DoGetEntities<T>(predicate, null).UpdateAsync(updateExpression);
        }

        public void DeleteEntity<T>(Expression<Func<T, bool>> predicate)
            where T : EntityBase, new()
        {
            var entities = this.Set<T>().Where(predicate).AsNoTracking().ToList();
            if (entities != null && entities.Any())
            {
                this.ChangeTracker.AutoDetectChangesEnabled = false;
                try
                {
                    RemoveRange(entities);
                }
                finally
                {
                    this.ChangeTracker.AutoDetectChangesEnabled = true;
                }
            }
        }

        public async Task DeleteEntityByCommitAsync<T>(Expression<Func<T, bool>> predicate)
            where T : EntityBase, new()
        {
            if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            await DoGetEntities<T>(predicate, null).DeleteAsync();
        }

        public void DeleteEntityByCommit<T>(Expression<Func<T, bool>> predicate)
            where T : EntityBase, new()
        {
            if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            DoGetEntities<T>(predicate, null).Delete();
        }

        public async Task AddEntitiesAsync<T>(List<T> entities)
         where T : EntityBase, new()
        {
            if (entities == null || !entities.Any())
            {
                return;
            }

            this.ChangeTracker.AutoDetectChangesEnabled = false;
            try
            {
                await this.AddRangeAsync(entities);
            }
            finally
            {
                this.ChangeTracker.AutoDetectChangesEnabled = true;
            }
        }

        public Task UpdateEntitiesAsync<T>(List<T> entities, List<T> entitiesDb, params string[] updateProperies)
          where T : EntityBase, new()
        {
            return Task.Run(() =>
            {
                if (entities == null || !entities.Any())
                {
                    return;
                }

                foreach (var entity in entities)
                {
                    var dbEntity = entitiesDb?.FirstOrDefault(t => t.id == entity.id);
                    if (dbEntity != null)
                    {
                        if (dbEntity.row_version != entity.row_version)
                        {
                            throw new EntityUpdateVersionException(string.Empty, "Update error,Please get and try again");
                        }
                    }

                    entity.row_version++;
                }

                this.ChangeTracker.AutoDetectChangesEnabled = false;
                try
                {
                    UpdateRange(entities);
                }
                finally
                {
                    this.ChangeTracker.AutoDetectChangesEnabled = true;
                }
            });
        }

        public async Task DeleteEntityAsync<T>(Expression<Func<T, bool>> predicate)
           where T : EntityBase, new()
        {
            var entities = await this.Set<T>().Where(predicate).AsNoTracking().ToArrayAsync();
            if (entities != null && entities.Any())
            {
                this.ChangeTracker.AutoDetectChangesEnabled = false;
                try
                {
                    this.RemoveRange(entities);
                }
                finally
                {
                    this.ChangeTracker.AutoDetectChangesEnabled = true;
                }
            }
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (this.commonContextParam.DBType == DataBaseTypeEnum.PostgreSQL)
            {
                optionsBuilder
                    .UseLazyLoadingProxies()
                    .UseNpgsql(this.commonContextParam.ConnString);
            }
            else if (this.commonContextParam.DBType == DataBaseTypeEnum.Oracle)
            {
                optionsBuilder.UseLazyLoadingProxies().UseOracle(this.commonContextParam.ConnString);
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            var ss = AppContext.BaseDirectory;
            ConfigurationModel(ss, modelBuilder);

            ResetModelBuilderModel(modelBuilder);
        }

        /// <summary>
        /// 重置Map数据
        /// </summary>
        /// <param name="modelBuilder"></param>
        private void ResetModelBuilderModel(ModelBuilder modelBuilder)
        {
            foreach (var model in modelBuilder.Model.GetEntityTypes())
            {
                var schema = model.GetSchema();
                var splitNames = model.Name.Split(".");
                var modelName = splitNames.Length >= 1 ? splitNames[splitNames.Length - 1] : model.Name;
                var tableName = model.GetTableName();
                if (modelName.ToUpper() != tableName && this.commonContextParam.DBType == DataBaseTypeEnum.Oracle)
                {
                    model.SetTableName(modelName.ToUpper());
                }

                var nullableClassAttrs = model.ClrType.GetCustomAttributes<EntityClassNullableAttribute>(true);
                var notnullableClassAttrs = model.ClrType.GetCustomAttributes<EntityClassNotNullableAttribute>(true);

                foreach (var property in model.GetProperties())
                {
                    var columnName = property.GetColumnName(StoreObjectIdentifier.Table(tableName, schema));
                    if (columnName != property.Name.ToUpper() && this.commonContextParam.DBType == DataBaseTypeEnum.Oracle)
                    {
                        property.SetColumnName(property.Name.ToUpper());
                    }

                    if (property.Name == "id")
                    {
                        if (this.commonContextParam.DBType == DataBaseTypeEnum.Oracle)
                        {
                            property.SetColumnType("VARCHAR2");
                        }
                    }
                    else if (property.ClrType == typeof(decimal))
                    {
                        if (this.commonContextParam.DBType == DataBaseTypeEnum.Oracle)
                        {
                            var scale = property.GetScale();
                            if (scale == null)
                            {
                                scale = 6;
                                property.SetScale(scale);
                            }

                            var precision = property.GetPrecision();
                            if (precision == null)
                            {
                                precision = 18;
                                property.SetPrecision(precision);
                            }

                            var colType = property.GetColumnType();
                            if (string.IsNullOrWhiteSpace(colType))
                            {
                                property.SetColumnType($"NUMBER({precision},{scale})");
                            }
                        }
                    }
                    else if (property.ClrType == typeof(string))
                    {
                        if (this.commonContextParam.DBType == DataBaseTypeEnum.Oracle)
                        {
                            property.IsNullable = true;
                        }

                        var isNullableByClass = nullableClassAttrs != null && nullableClassAttrs.Any(t => t.Fields != null && t.Fields.Any(t1 => t1 == property.Name));
                        var isNotNullableByClass = notnullableClassAttrs != null && notnullableClassAttrs.Any(t => t.Fields != null && t.Fields.Any(t1 => t1 == property.Name));

                        object[] nullableAttrs = property.PropertyInfo != null ? property.PropertyInfo.GetCustomAttributes(typeof(EntityFieldNullableAttribute), true) : null;
                        var isNullable = nullableAttrs != null && nullableAttrs.Any();
                        if (!isNullable && property.IsIndex())
                        {
                            if (!isNullableByClass)
                            {
                                property.IsNullable = false;
                                property.SetDefaultValue(string.Empty);
                            }
                        }
                        else
                        {
                            var tempIsNotNull = isNotNullableByClass;
                            if (property.PropertyInfo != null)
                            {
                                object[] attrs = property.PropertyInfo.GetCustomAttributes(typeof(EntityFieldNotNullableAttribute), true);
                                if (attrs != null && attrs.Any())
                                {
                                    tempIsNotNull = true;
                                }
                            }

                            if (tempIsNotNull)
                            {
                                property.IsNullable = false;
                                property.SetDefaultValue(string.Empty);
                            }
                        }
                    }
                    else if (property.ClrType == typeof(DateTime?))
                    {
                        if (property.Name != "create_time" && property.Name != "modify_time" && property.IsIndex())
                        {
                            throw new Exception($"table {tableName} field {property.Name} datetime? can not index");
                        }
                    }
                }
            }
        }

        private void ConfigurationModel(string path, ModelBuilder modelBuilder)
        {
            if (string.IsNullOrEmpty(path))
            {
                return;
            }

            var patterns = new List<string>();
            patterns.Add("*.Data.dll");
            foreach (var searchP in patterns)
            {
                if (string.IsNullOrEmpty(searchP))
                {
                    continue;
                }

                string[] files = Directory.GetFiles(path, searchP);
                if (!files.Any())
                {
                    continue;
                }

                foreach (var assemblyFile in files)
                {
                    Assembly assembly = Assembly.LoadFrom(assemblyFile);
                    if (assembly != null)
                    {
                        modelBuilder.ApplyConfigurationsFromAssembly(assembly);
                    }
                }
            }
        }

        private IQueryable<T> DoGetEntities<T>(Expression<Func<T, bool>> predicate, string[] includes)
            where T : EntityBase
        {
            IQueryable<T> queryable = this.Set<T>();
            if (includes != null && includes.Any())
            {
                foreach (var item in includes)
                {
                    queryable = queryable.Include(item);
                }
            }

            if (predicate != null)
            {
                queryable = queryable.Where(predicate);
            }

            queryable = queryable.AsNoTracking();
            return queryable;
        }
    }
}
